<浏览器>js执行机制

javascript在浏览器中的执行机制

变量提升

变量提升、执行流程

声明、赋值

1
2
3
4
5
var a;//变量声明
a = 1;//变量赋值

function fun1(){}//函数声明
var fun2 = ()=>{}//函数赋值

变量提升

  • 所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

JavaScript 代码的执行流程

  • 实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。
  • JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。
  1. 编译阶段
  • 可以将代码分为变量提升部分执行部分
  • 输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码
  • 执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。
  • 在执行上下文中存在一个变量环境的对象,保存了变量提升的内容。
  • 变量声明:经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个的属性,并使用 undefined 对其初始化
  • 函数声明:JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建对应属性,然后将该属性值指向堆中函数的位置
  1. 执行阶段
  • 当执行到对应属性时,JavaScript 引擎便开始在变量环境对象中查找该属性,执行函数或查找值
  1. 注意情况
  • 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
  • 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略

调用栈

调用栈

  • 调用栈是用来管理函数调用关系的一种数据结构。

JavaScript 的调用栈

  • js用栈结构管理执行上下文
  1. 创建全局上下文,并将其压入栈底
  2. 执行函数时,创建函数一个执行上下文,并压入栈中
  3. 当函数返回时,该函数的执行上下文就会从栈顶弹出
  • 可以使用 console.trace() 来输出当前的函数调用关系

栈溢出

  • 调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。

作用域

  • 作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

js中的作用域

  • 全局作用域,生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域,ES6新增,块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

变量提升问题

  • 变量会被覆盖
1
2
3
4
5
6
7
8
var a = 1
function fun(){
if(false){
var a = 2 // 变量提升至函数作用域中
}
console.log(a)
}
fun() // log undefined
  • 变量没有被销毁
1
2
3
4
5
function fun(){
for(var a = 1;a<5;a++){}
console.log(a) // log 5
}
fun()

es6解决变量提升缺陷

  • ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。
  • 执行上下文的角度解释es6块级作用域
  1. 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里。
  2. 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
  3. 作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量。
  4. 在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。
  5. 变量查找方法:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
  6. 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出

作用域链和闭包

作用域链

闭包

作用域链

  • 每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。
  • JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
  • 词法作用域:词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
  • 词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

闭包

  • 在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
  • 闭包是如何回收
  1. 如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
  2. 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
  3. 如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
  • 内存模型解释闭包:当js引擎发现闭包引用时,会在堆空间中创建闭包对象,用来保存闭包引用的值,执行上下文中创建闭包内部变量,引用闭包对象的地址。

js中的this

  • this 是和执行上下文绑定
  • 全局对象中的 this 是指向 window 对象
  • 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
  • 当函数作为对象的方法调用时,函数中的 this 就是该对象;
  • 嵌套函数中的 this 不会继承外层函数的 this 值,可以通过声明一个变量保存this,或者箭头函数解决
  • 箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this。
  • 可以通过call、bind和apply方法改变this
  • 构造函数的this
  1. 首先创建了一个空对象 tempObj;
  2. 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
  3. 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
  4. 最后返回 tempObj 对象。strong text